[Suspense] Document what activates a boundary and define Suspense-enabled frameworks#8505
[Suspense] Document what activates a boundary and define Suspense-enabled frameworks#8505aurorascharff wants to merge 11 commits into
Conversation
Size changesDetails📦 Next.js Bundle Analysis for react-devThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
There was a problem hiding this comment.
Pull request overview
Updates the <Suspense> reference to replace an outdated note with a new, linkable section that enumerates the different kinds of work/resources that can activate a Suspense boundary, while keeping data-fetching caveats in a <Note>.
Changes:
- Adds a new “What activates a Suspense boundary” subsection with a broader activation list (e.g.,
lazy,use, stylesheets, fonts, images, SSR streaming, experimentaldefer). - Moves data-fetching caveats into a
<Note>and tweaks wording around framework support (“built-in Suspense support”). - Removes Next.js from the earlier “Suspense-enabled frameworks” mention in favor of describing Server Components /
use()behavior.
Docs review checklist (by guide)
docs-writer-reference
-
src/content/reference/react/Suspense.md:217Bullet uses<img onLoad>-style wording that reads like JSX but isn’t valid JSX and is a bit unclear/contradictory in context.
docs-voice
-
src/content/reference/react/Suspense.md:217Consider rephrasing to use inline code for the prop name (onLoad) and clarify the “not enabled by default” vs “opts out” wording.
docs-components
- No issues found in the changed region (callout spacing and section divider usage look consistent).
docs-sandpack
- No issues found related to Sandpack formatting/content in the changed region.
Please create a plan to address the unchecked checklist item(s).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <Note> | ||
| --- | ||
|
|
||
| ### What activates a Suspense boundary {/*what-activates-a-suspense-boundary*/} |
There was a problem hiding this comment.
Very excited to see these being called out and updated!
My understanding is that a "suspense enabled framework" is a framework that wraps these features that activates a suspense boundaries . If that is true could we specifically call it out what suspense enabled means?
| The exact way you would load data in the `Albums` component above depends on your framework. If you use a Suspense-enabled framework, you'll find the details in its data fetching documentation. | ||
| The exact way you would load data in the `Albums` component above depends on your framework. If you use a framework with built-in Suspense support, you'll find the details in its data fetching documentation. | ||
|
|
||
| Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React. |
There was a problem hiding this comment.
This line is out of date because of yours and @rickhanlonii great work on the use() docs.
There was a problem hiding this comment.
Nice catch!!! Updating!
| - If Suspense was displaying content for the tree, but then it suspended again, the `fallback` will be shown again unless the update causing it was caused by [`startTransition`](/reference/react/startTransition) or [`useDeferredValue`](/reference/react/useDeferredValue). | ||
| - If React needs to hide the already visible content because it suspended again, it will clean up [layout Effects](/reference/react/useLayoutEffect) in the content tree. When the content is ready to be shown again, React will fire the layout Effects again. This ensures that Effects measuring the DOM layout don't try to do this while the content is hidden. | ||
| - React includes under-the-hood optimizations like *Streaming Server Rendering* and *Selective Hydration* that are integrated with Suspense. Read [an architectural overview](https://github.com/reactwg/react-18/discussions/37) and watch [a technical talk](https://www.youtube.com/watch?v=pj5N-Khihgc) to learn more. | ||
|
|
There was a problem hiding this comment.
I opened issue #8490 about the need for a caveat regarding React's batching of Suspense content, which has resulted in several issues in the React repo that can be solved with documentation. If we can add it to this pr, it would be great. I am thinking something like this:
| - React waits at least ~300ms before revealing a `Suspense` fallback, and applies the same throttle across different boundaries. This reduces flicker when nested boundaries resolve in quick succession, which is common during [streaming server rendering](/reference/react-dom/server). Content that becomes ready within that window is held and revealed together once the window closes. A boundary that resolves before its fallback is committed—for example, synchronously, or within roughly 10ms—may skip the fallback entirely. | |
There was a problem hiding this comment.
React waits at least 300ms before replacing the fallback with new content. That's different to blocking display of a fallback which only happens during updates in Transitions.
The idea is that filling in content more often leads to a "popcorn" effect. Imagine a 10x10 grid where each cell pops in 50ms intervals instead of doing 2-3 reveals including multiple cells. 300ms is just a heuristic that allows batching reveal of multiple boundaries.
There was a problem hiding this comment.
I like your caveat much better than my suggestion (great job!). But I feel like it implies that suspense will always show a fallback.
My understanding is that
0ms to ~10 ms - no fallback; suspense just renders children
11ms to ~300ms - batch at call; all render all together after 300ms
~310ms+ render in 300ms windows (not sure about this one)
This is figuring out an ideal rendering schedule; it is very impressive work by the React team, but is a little hard to explain. I hope I am not derailing the goals of this pr.
There was a problem hiding this comment.
Would need @eps1lon here to confirm and define what docs should include
There was a problem hiding this comment.
The throttling heuristic always blocks new boundary reveals counting from the last reveal. It has no impact on whether we display a fallback or not which is decided independently (always show fallback on mount, stay on content on update).
The very first boundary React will always reveal asap. If the boundary suspended, React shows a fallback immediately. When the boundary unsuspends, React will not reveal the boundaries content before 300ms have elapsed since the last reveal.
Throttling is surfaced in the performance tracks added by React.
Consider this example (using https://codesandbox.io/p/sandbox/quiet-http-yplr4n):
function App({ p1, p2, p3 }) {
return (
<main>
<h1>document</h1>
<p>shell </p>
<Suspense fallback="Loading lvl 1">
<p>boundary lvl</p>
<Suspender promise={p1} />
<Suspense fallback="Loading lvl 2">
<p>boundary lvl2</p>
<Suspender promise={p2} />
<Suspense fallback="Loading lvl 3">
<p>boundary lvl3</p>
<Suspender promise={p3} />
</Suspense>
</Suspense>
</Suspense>
</main>
);
}All Promises start at the same time as we can commit shell (which doesn't suspend and is committed instantly).
p1 resolves at 100ms
p2 resolves at 450ms
p3 resolves at 850ms
When p1 resolves we committed the root boundary (what we commonly refer to as "the shell") 100ms earlier. Since we don't want to reveal more often than every 300ms, we throttle for 200ms and reveal lvl 1 at 300ms.
When p2 resolves at 450ms, lvl1 was revealed 150ms earlier. Since we don't want to reveal more often than every 300ms, we throttle for 150ms and reveal lvl 2 at 600ms.
When p3 resolves at 850ms, lv2 was revealed 250ms earlier. Since we don't want to reveal more often than every 300ms, we throttle for 50ms and reveal lvl 2 at 900ms.
CleanShot.2026-07-01.at.14.59.48.mp4
The batching can be observed when you let two boundaries resolve within the same 300ms window e.g.
p1 resolves at 100ms
p2 resolves at 350ms
p3 resolves at 450ms
p1 is throttled for 200ms
p2 is throttled for 250ms
p3 is throttled for 150ms
p2 and p3 are revealed together.
CleanShot.2026-07-01.at.15.05.25.mp4
Without throttling all would commit independently within a short timeframe.
Throttling is global so it doesn't just apply to parent-child reveals. Initially, React throttled to 500ms which got reduced to 300ms in React 19. In React 19 we also applied throttling more broadly. The initial version of Suspense revealed boundaries immediately if there were no more nested boundaries.
| - Reading a Promise with [`use`](/reference/react/use), including data streamed from [Server Components](/reference/rsc/server-components) and integrations from frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/). | ||
| - Loading a stylesheet rendered with [`<link rel="stylesheet">` and a `precedence` prop.](/reference/react-dom/components/link#special-rendering-behavior) React blocks the boundary until the stylesheet loads, up to a timeout. | ||
| - Loading fonts. React blocks a streamed boundary until [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) resolves, up to a timeout. Fonts also block a [`<ViewTransition>`](/reference/react/ViewTransition) update. | ||
| - Streaming a large boundary's HTML during server rendering. React reveals the content as the HTML arrives. |
There was a problem hiding this comment.
The other examples are probably self-explanatory. This one could do with an example. Examples for all would be nice with a side-by-side with a Suspense implementation that wouldn't block (e.g. with just some simple DOM operations).
There was a problem hiding this comment.
How about with effect as an example? It's a common confusion
There was a problem hiding this comment.
That's for use() vs fetching in an Effect though. I was thinking about a boundary with a lot of text. There are sandboxes in the prerender docs (or resume. Can't remember but we do have one example for PPR in the docs) that show how to build sandboxes showing streaming SSR.
| - Lazy-loading component code with [`lazy`](/reference/react/lazy). | ||
| - Reading a Promise with [`use`](/reference/react/use), including data streamed from [Server Components](/reference/rsc/server-components) and integrations from frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/). | ||
| - Loading a stylesheet rendered with [`<link rel="stylesheet">` and a `precedence` prop.](/reference/react-dom/components/link#special-rendering-behavior) React blocks the boundary until the stylesheet loads, up to a timeout. | ||
| - Loading fonts. React blocks a streamed boundary until [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) resolves, up to a timeout. Fonts also block a [`<ViewTransition>`](/reference/react/ViewTransition) update. |
There was a problem hiding this comment.
Is that clear that this only applies to boundaries that are revealed by new SSR content arriving?
Why:
The Suspense reference said only Suspense-enabled data sources activate a boundary. That's outdated (many things activate Suspense now), and the term "Suspense-enabled framework" was used across several pages without ever being defined. The note also claimed framework-less data fetching was unsupported and undocumented, which the recent
use()docs have since made untrue. The same stale note was copied into the SSR API pages too.What:
lazy,use, stylesheets, fonts, streamed HTML, images, and deferred CPU work.use()with a cached Promise.use()rather than a custom integration.use) with one that doesn't (fetches in an Effect).renderToPipeableStream,renderToReadableStream,prerender,prerenderToNodeStream) andActivitydown to a short pointer to the Suspense section, instead of inlining the whole thing everywhere.The behavior and framing were verified by the React team.